package org.cerberus.platform.internal.impl;

import java.lang.ref.Reference;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.map.ReferenceMap;
import org.cerberus.platform.internal.IObjectGraph;
import org.cerberus.platform.internal.IObjectGraphTransaction;
import org.cerberus.platform.internal.IPlatformClassLoader;
import org.cerberus.platform.internal.domain.IComponentClass;
import org.cerberus.platform.internal.domain.IComponentClassAttribute;

/**
 * 
 * @author Christian Humer
 */
public class ObjectGraph implements IObjectGraph {

	private static final Integer MINUS_ONE = new Integer(-1);
	
	private Object root;
	
	private int objectCount;
	
	Map<Integer, Object> objectMap;
	Map<Object, Integer> objectIdentificationMap;
	
	private IPlatformClassLoader classLoader; 
	private IPlatformClassLoader.LoaderCallback loaderCallback;
	private IPlatformClassLoader.UnLoaderCallback unloaderCallback;
	
	private IObjectGraphTransaction transaction;
	
	@SuppressWarnings("unchecked")
	public ObjectGraph() {
//		 objectMap = new HashMap<Integer, Object>(130000);
//		 objectIdentificationMap = new HashMap<Object, Integer>(130000);

		objectMap = new ObjectGraphReferenceMap(ReferenceMap.HARD,
				ReferenceMap.WEAK, 50000, 0.75f, true);
		objectIdentificationMap = new ObjectGraphReferenceMap(
				ReferenceMap.WEAK, ReferenceMap.HARD, 50000, 0.75f, true);
		transaction = createTransaction();
		classLoader = new PlatformClassLoader();
		loaderCallback = new IPlatformClassLoader.LoaderCallback() {
			@Override
			public void loaded(IComponentClass clazz) {
				classAdded(clazz);
			}
		};
		unloaderCallback = new IPlatformClassLoader.UnLoaderCallback() {
			@Override
			public void unloaded(IComponentClass clazz) {
				classRemoved(clazz);
			}
		};
	}
	
	/**
	 * @return
	 * @uml.property  name="root"
	 */
	@Override
	public Object getRoot() {
		return root;
	}

	/**
	 * @param o
	 * @uml.property  name="root"
	 */
	@Override
	public void setRoot(Object o) {
		if (transaction == null) {
			throw new ComponentClassException("Object graph transaction not started.");
		}
		if (this.root != null) {
			Object oldRoot = this.root;
			this.root = null;
			nodeRemoved(oldRoot);
		}
		if (o != null) {
			this.root = o;
			nodeAdded(o);
		}
	}
	
	private void classAdded(IComponentClass componentClass) {
		transaction.addComponentClass(componentClass);
	}

	private void classRemoved(IComponentClass componentClass) {
		transaction.removeComponentClass(componentClass.getClassID());
	}

	
	private void nodeAdded(Object o) {
		// if array
		if (o instanceof Object[]) {
			Object[] array = (Object[])o;
			if (!SystemClasses.isNativeArray(array)) {
				for (int i = 0; i < array.length; i++) {
					Object arrayElement = array[i];
					if (arrayElement != null) {
						if (!SystemClasses.isNativeClass(arrayElement.getClass())) {
							nodeAdded(arrayElement);
						}
					}
				}
			}
		} else {
			Class<?> objectClass = o.getClass();
			IComponentClass componentClass = classLoader.defineClass(objectClass, loaderCallback);
			if (componentClass == null || !componentClass.isDeclared()) {
				throw new ComponentClassException("Class "+o.getClass().getName()+" could not be defined as PComponent.");
			}
			Integer hash = hashObject(o);
			if (!objectMap.containsKey(hash)) {
				objectMap.put(hash, o);
				objectIdentificationMap.put(o, hash);
				classLoader.increment(componentClass, loaderCallback);
				transaction.addObject(hash, createObjectData(componentClass, o));
				walkNodeAttributesAdd(componentClass, o);
			}
		}
	}
	
	private Integer hashObject(Object o) {
		if (objectIdentificationMap.containsKey(o)) {
			return objectIdentificationMap.get(o);
		}
		return ++objectCount;
	}
	
	private void nodeRemoved(Object o) {
		if (o instanceof Object[]) {
			Object[] array = (Object[])o;
			for (int i = 0; i < array.length; i++) {
				Object arrayElement = array[i];
				if (arrayElement != null) {
					if (!SystemClasses.isNativeClass(arrayElement.getClass())) {
						nodeRemoved(arrayElement);
					}
				}
			}
		} else {
			Class<?> objectClass = o.getClass();
			if (!classLoader.isLoaded(objectClass)) {
				// unknown in any way
				return;
			}
			Integer hash = hashObject(o);
			if (objectMap.containsKey(hash)) {
				IComponentClass clazz = classLoader.defineClass(objectClass, loaderCallback);
				
				objectMap.remove(hash);
				objectIdentificationMap.remove(o);
				transaction.removeObject(hash);
				
				walkNodeAttributesRemove(clazz, o);
				
				classLoader.decrement(clazz, unloaderCallback);
			}
		}
	}

	private void walkNodeAttributesAdd(IComponentClass clazz, Object o) {
		if (!clazz.isPrimitive() && clazz.getAttributeCount() > 0) {
			return;
		}
		IComponentClassAttribute[] attributes = clazz.getAttributes();
		for (int i = 0; i < attributes.length; i++) {
			IComponentClassAttribute componentClassAttribute = attributes[i];
			if (!componentClassAttribute.getAttributeClass().isPrimitive()) {
				Object value = getValueImpl(o, componentClassAttribute);
				if (value != null) {
					// if array
					nodeAdded(value);
				}
			}
		}
	}

	private void walkNodeAttributesRemove(IComponentClass clazz, Object o) {
		if (clazz.getAttributeCount() == 0) {
			return;
		}
		IComponentClassAttribute[] attributes = clazz.getAttributes();
		for (int i = 0; i < attributes.length; i++) {
			IComponentClassAttribute componentClassAttribute = attributes[i];
			if (!componentClassAttribute.getAttributeClass().isPrimitive()) {
				Object value = getValueImpl(o, componentClassAttribute);
				if (value != null) {
					nodeRemoved(value);
				}
			}
		}
	}

	@Override
	public void notifyChange(Object o, String attributeName) {
		Integer hash = getInternalID(o); 
		// not tracked by graph -> do nothing
		if (hash == MINUS_ONE) {
			return;
		}
		if (transaction == null) {
			throw new ComponentClassException("Object graph transaction not started.");
		}
		IComponentClass clazz = classLoader.defineClass(o.getClass(), loaderCallback);
		if (clazz == null) {
			throw new ComponentClassException("Class not found for object "+o);
		}
		IComponentClassAttribute attribute = clazz.getAttribute(attributeName);
		if (attribute == null) {
			throw new ComponentClassException("Could not find attribute "+attributeName+" for class "+o.getClass().getName());
		}
		// if component attribute we need to track down
		// references
		Object attributeValue = getValueImpl(o, attribute);
		
		if (!attribute.getAttributeClass().isPrimitive()) {
			if (attributeValue != null) {
				nodeAdded(attributeValue);
			}
		}
		
		attributeValue = createSingleObjectData(clazz, attribute, o);
		transaction.addObjectChange(hash, attribute.getIndex(), clazz.getAttributeCount(), attributeValue);
	}

	@Override
	public Integer getInternalID(Object o) {
		Integer hash = objectIdentificationMap.get(o);
		if (hash == null) {
			return MINUS_ONE;
		}
		return hash;
	}
	
	private Object[] createObjectData(IComponentClass clazz, Object o) {
		IComponentClassAttribute[] attributes = clazz.getAttributes();
		Object[] data = new Object[attributes.length+1];
		data[0] = clazz.getClassID();
		for (int i = 1; i < attributes.length+1; i++) {
			data[i] = createSingleObjectData(clazz, (ComponentClassAttribute)attributes[i-1], o);
		}
		return data;
	}
	
	private Object createSingleObjectData(IComponentClass clazz, IComponentClassAttribute attribute, Object o) {
		if (attribute.getAttributeClass().isPrimitive()) {
			return getValueImpl(o, attribute);
		} else {
			Object child = getValueImpl(o, attribute);
			if (child != null) {
				if (child instanceof Object[]) {
					return recurseResolve((Object[])child);
				} else {
					return getInternalID(child);
				}
			} else {
				return null;
			}
		}
	}
	
	private Object[] recurseResolve(Object[] array) {
		Object[] clone = new Object[array.length];
		
		// optimization if native array we use array copy
		if (SystemClasses.isNativeArray(array)) {
			System.arraycopy(array, 0, clone, 0, array.length);
			return clone;
		}
		
		for (int i = 0; i < array.length; i++) {
			Object object = array[i];
			int internalID = getInternalID(object);
			if (internalID == -1) {
				if (object instanceof Object[]) {
					clone[i] = recurseResolve((Object[])object);
				} else {
					clone[i] = object;
				}
			} else {
				clone[i] = internalID;
			}
		}
		return clone;
	}
	
	private Object getValueImpl( Object o, IComponentClassAttribute attribute) {
		Object value = null;
		try {
			value = attribute.get(o);
		} catch (ComponentClassException e) {
			throw e;
		} catch (Throwable e) {
			handleUserException(e);
		}
		return value;
	}
	
	private void handleUserException(Throwable t) {
		t.printStackTrace();
		//TODO better handling -> generic handling for platform
	}
	
	protected IObjectGraphTransaction createTransaction() {
		return new InMemoryObjectGraphTransaction();
	}
	
	@Override
	public void storeTransaction(IObjectGraphTransaction transaction) {
		//TODO implement me
	}
	
	public synchronized IObjectGraphTransaction returnTransaction() {
		//TODO check if purging objects should be thread safe
		Set<Integer> purgedValues = ((ObjectGraphReferenceMap)objectMap).purgeManually();
		if (purgedValues != null && purgedValues.size() > 0) {
			transaction.removeObjects(purgedValues);
		}
		
		// should be thread safe 
		// because this method is often called asynchronous
		IObjectGraphTransaction returnTransaction = transaction;
		this.transaction = createTransaction();
		return returnTransaction;
	}
	
	@Override
	public void initializeClass(Class<?> clazz) {
		classLoader.defineClass(clazz, loaderCallback);
	}
	
	@Override
	public IComponentClass getComponentClass(Class<?> clazz) {
		return classLoader.defineClass(clazz, loaderCallback);
	}
	
	static class ObjectGraphReferenceMap extends ReferenceMap {
		
		private static final long serialVersionUID = 1L;
		
		private Set<Integer> purgedValues = new HashSet<Integer>();
		
		public ObjectGraphReferenceMap(int keyType, int valueType,
				int capacity, float loadFactor, boolean purgeValues) {
			super(keyType, valueType, capacity, loadFactor, purgeValues);
		}

		@Override
		public void purge() {
			super.purge();
		}
		public Set<Integer> purgeManually() {
			super.purge();
			super.purge();
			
			if (purgedValues.size() > 0) {
				Set<Integer> purged = purgedValues;
				purgedValues = new HashSet<Integer>();
				return purged;
			}
			return purgedValues;
		}
		
	    @SuppressWarnings("unchecked")
		protected void purge(Reference ref) {
	        // The hashCode of the reference is the hashCode of the
	        // mapping key, even if the reference refers to the 
	        // mapping value...
	    	purgedValues.add(ref.hashCode());
	        super.purge(ref);
	    }
		
		@Override
		protected void purgeBeforeRead() {
		}
		
		@Override
		protected void purgeBeforeWrite() {
		}
		
	}
}
